/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.archetype.common.util;

/*
 * Copyright (C) 2000-2004 Jason Hunter & Brett McLaughlin.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions, and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions, and the disclaimer that follows
 *    these conditions in the documentation and/or other materials
 *    provided with the distribution.
 *
 * 3. The name "JDOM" must not be used to endorse or promote products
 *    derived from this software without prior written permission.  For
 *    written permission, please contact <request_AT_jdom_DOT_org>.
 *
 * 4. Products derived from this software may not be called "JDOM", nor
 *    may "JDOM" appear in their name, without prior written permission
 *    from the JDOM Project Management <request_AT_jdom_DOT_org>.
 *
 * In addition, we request (but do not require) that you include in the
 * end-user documentation provided with the redistribution and/or in the
 * software itself an acknowledgement equivalent to the following:
 *     "This product includes software developed by the
 *      JDOM Project (http://www.jdom.org/)."
 * Alternatively, the acknowledgment may be graphical using the logos
 * available at http://www.jdom.org/images/logos.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE JDOM AUTHORS OR THE PROJECT
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the JDOM Project and was originally
 * created by Jason Hunter <jhunter_AT_jdom_DOT_org> and
 * Brett McLaughlin <brett_AT_jdom_DOT_org>.  For more information
 * on the JDOM Project, please see <http://www.jdom.org/>.
 */

import javax.xml.transform.Result;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.List;

import org.codehaus.plexus.util.StringUtils;
import org.jdom2.Attribute;
import org.jdom2.CDATA;
import org.jdom2.Comment;
import org.jdom2.DocType;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.EntityRef;
import org.jdom2.Namespace;
import org.jdom2.ProcessingInstruction;
import org.jdom2.Text;
import org.jdom2.output.EscapeStrategy;

/**
 * <p>This class is a fork from JDOM 1.0 modified to preserve CData sections and
 * comments.</p>
 *
 * <p>Outputs a JDOM document as a stream of bytes. The outputter can manage many
 * styles of document formatting, from untouched to pretty printed. The default
 * is to output the document content exactly as created, but this can be changed
 * by setting a new Format object. For pretty-print output, use
 * <code>{@link Format#getPrettyFormat()}</code>. For whitespace-normalized
 * output, use <code>{@link Format#getCompactFormat()}</code>.</p>
 *
 * <p>There are <code>{@link #output output(...)}</code> methods to print any of
 * the standard JDOM classes, including Document and Element, to either a Writer
 * or an OutputStream. <b>Warning</b>: When outputting to a Writer, make sure
 * the writer's encoding matches the encoding setting in the Format object. This
 * ensures the encoding in which the content is written (controlled by the
 * Writer configuration) matches the encoding placed in the document's XML
 * declaration (controlled by the XMLOutputter). Because a Writer cannot be
 * queried for its encoding, the information must be passed to the Format
 * manually in its constructor or via the
 * <code>{@link Format#setEncoding}</code> method. The default encoding is
 * UTF-8.</p>
 *
 * <p>The methods <code>{@link #outputString outputString(...)}</code> are for
 * convenience only; for top performance you should call one of the <code>{@link
 * #output output(...)}</code> methods and pass in your own Writer or
 * OutputStream if possible.</p>
 *
 * <p>XML declarations are always printed on their own line followed by a line
 * separator (this doesn't change the semantics of the document). To omit
 * printing of the declaration use
 * <code>{@link Format#setOmitDeclaration}</code>. To omit printing of the
 * encoding in the declaration use <code>{@link Format#setOmitEncoding}</code>.
 * Unfortunately there is currently no way to know the original encoding of the
 * document.</p>
 *
 * <p>Empty elements are by default printed as &lt;empty/&gt;, but this can be
 * configured with <code>{@link Format#setExpandEmptyElements}</code> to cause
 * them to be expanded to &lt;empty&gt;&lt;/empty&gt;.</p>
 *
 * @author Brett McLaughlin
 * @author Jason Hunter
 * @author Jason Reid
 * @author Wolfgang Werner
 * @author Elliotte Rusty Harold
 * @author David &amp; Will (from Post Tool Design)
 * @author Dan Schaffer
 * @author Alex Chaffee
 * @author Bradley S. Huffman
 */
public class XMLOutputter implements Cloneable {
    // For normal output
    private Format userFormat = Format.getRawFormat();

    // For xml:space="preserve"
    protected static final Format PRESERVE_FORMAT = Format.getRawFormat();

    // What's currently in use
    protected Format currentFormat = userFormat;

    /**
     * Whether output escaping is enabled for the being processed
     * Element - default is <code>true</code>
     */
    private boolean escapeOutput = true;

    // * * * * * * * * * * Constructors * * * * * * * * * *
    // * * * * * * * * * * Constructors * * * * * * * * * *

    /**
     * This will create an <code>XMLOutputter</code> with the default
     * {@link Format} matching {@link Format#getRawFormat}.
     */
    public XMLOutputter() {}

    /**
     * This will create an <code>XMLOutputter</code> with the specified
     * format characteristics.  Note the format object is cloned internally
     * before use.
     */
    public XMLOutputter(Format format) {
        userFormat = (Format) format.clone();
        currentFormat = userFormat;
    }

    /**
     * This will create an <code>XMLOutputter</code> with all the
     * options as set in the given <code>XMLOutputter</code>.  Note
     * that <code>XMLOutputter two = (XMLOutputter)one.clone();</code>
     * would work equally well.
     *
     * @param that the XMLOutputter to clone
     */
    public XMLOutputter(XMLOutputter that) {
        this.userFormat = (Format) that.userFormat.clone();
        currentFormat = userFormat;
    }

    // * * * * * * * * * * Set parameters methods * * * * * * * * * *
    // * * * * * * * * * * Set parameters methods * * * * * * * * * *

    /**
     * Sets the new format logic for the outputter.  Note the Format
     * object is cloned internally before use.
     *
     * @param newFormat the format to use for output
     */
    public void setFormat(Format newFormat) {
        this.userFormat = (Format) newFormat.clone();
        this.currentFormat = userFormat;
    }

    /**
     * Returns the current format in use by the outputter.  Note the
     * Format object returned is a clone of the one used internally.
     */
    public Format getFormat() {
        return (Format) userFormat.clone();
    }

    // * * * * * * * * * * Output to a OutputStream * * * * * * * * * *
    // * * * * * * * * * * Output to a OutputStream * * * * * * * * * *

    /**
     * This will print the <code>Document</code> to the given output stream.
     * The characters are printed using the encoding specified in the
     * constructor, or a default of UTF-8.
     *
     * @param doc <code>Document</code> to format.
     * @param out <code>OutputStream</code> to use.
     * @throws IOException - if there's any problem writing.
     */
    public void output(Document doc, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(doc, writer); // output() flushes
    }

    /**
     * Print out the <code>{@link DocType}</code>.
     *
     * @param doctype <code>DocType</code> to output.
     * @param out     <code>OutputStream</code> to use.
     */
    public void output(DocType doctype, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(doctype, writer); // output() flushes
    }

    /**
     * Print out an <code>{@link Element}</code>, including
     * its <code>{@link Attribute}</code>s, and all
     * contained (child) elements, etc.
     *
     * @param element <code>Element</code> to output.
     * @param out     <code>Writer</code> to use.
     */
    public void output(Element element, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(element, writer); // output() flushes
    }

    /**
     * This will handle printing out an <code>{@link
     * Element}</code>'s content only, not including its tag, and
     * attributes.  This can be useful for printing the content of an
     * element that contains HTML, like "&lt;description&gt;JDOM is
     * &lt;b&gt;fun&gt;!&lt;/description&gt;".
     *
     * @param element <code>Element</code> to output.
     * @param out     <code>OutputStream</code> to use.
     */
    public void outputElementContent(Element element, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        outputElementContent(element, writer); // output() flushes
    }

    /**
     * This will handle printing out a list of nodes.
     * This can be useful for printing the content of an element that
     * contains HTML, like "&lt;description&gt;JDOM is
     * &lt;b&gt;fun&gt;!&lt;/description&gt;".
     *
     * @param list <code>List</code> of nodes.
     * @param out  <code>OutputStream</code> to use.
     */
    public void output(List<?> list, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(list, writer); // output() flushes
    }

    /**
     * Print out a <code>{@link CDATA}</code> node.
     *
     * @param cdata <code>CDATA</code> to output.
     * @param out   <code>OutputStream</code> to use.
     */
    public void output(CDATA cdata, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(cdata, writer); // output() flushes
    }

    /**
     * Print out a <code>{@link Text}</code> node.  Perfoms
     * the necessary entity escaping and whitespace stripping.
     *
     * @param text <code>Text</code> to output.
     * @param out  <code>OutputStream</code> to use.
     */
    public void output(Text text, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(text, writer); // output() flushes
    }

    /**
     * Print out a <code>{@link Comment}</code>.
     *
     * @param comment <code>Comment</code> to output.
     * @param out     <code>OutputStream</code> to use.
     */
    public void output(Comment comment, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(comment, writer); // output() flushes
    }

    /**
     * Print out a <code>{@link ProcessingInstruction}</code>.
     *
     * @param pi  <code>ProcessingInstruction</code> to output.
     * @param out <code>OutputStream</code> to use.
     */
    public void output(ProcessingInstruction pi, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(pi, writer); // output() flushes
    }

    /**
     * Print out a <code>{@link EntityRef}</code>.
     *
     * @param entity <code>EntityRef</code> to output.
     * @param out    <code>OutputStream</code> to use.
     */
    public void output(EntityRef entity, OutputStream out) throws IOException {
        Writer writer = makeWriter(out);
        output(entity, writer); // output() flushes
    }

    /**
     * Get an OutputStreamWriter, using prefered encoding
     * (see {@link Format#setEncoding}).
     */
    private Writer makeWriter(OutputStream out) throws java.io.UnsupportedEncodingException {
        return makeWriter(out, userFormat.encoding);
    }

    /** Get an OutputStreamWriter, use specified encoding. */
    private static Writer makeWriter(OutputStream out, String enc) throws java.io.UnsupportedEncodingException {
        return new BufferedWriter((new OutputStreamWriter(new BufferedOutputStream(out), enc)));
    }

    // * * * * * * * * * * Output to a Writer * * * * * * * * * *
    // * * * * * * * * * * Output to a Writer * * * * * * * * * *

    /**
     * <p>This will print the <code>Document</code> to the given Writer.</p>
     *
     * <p>Warning: using your own Writer may cause the outputter's
     * preferred character encoding to be ignored.  If you use
     * encodings other than UTF-8, we recommend using the method that
     * takes an OutputStream instead.</p>
     *
     * @param doc <code>Document</code> to format.
     * @param out <code>Writer</code> to use.
     * @throws IOException - if there's any problem writing.
     */
    public void output(Document doc, Writer out) throws IOException {

        printDeclaration(out, doc, userFormat.encoding);

        // Print out root element, as well as any root level
        // comments and processing instructions,
        // starting with no indentation
        List<?> content = doc.getContent();
        int size = content.size();
        for (int i = 0; i < size; i++) {
            Object obj = content.get(i);

            if (obj instanceof Element) {
                printElement(out, doc.getRootElement(), 0, createNamespaceStack());
            } else if (obj instanceof Comment) {
                printComment(out, (Comment) obj);
            } else if (obj instanceof ProcessingInstruction) {
                printProcessingInstruction(out, (ProcessingInstruction) obj);
            } else if (obj instanceof DocType) {
                printDocType(out, doc.getDocType());
                // Always print line separator after declaration, helps the
                // output look better and is semantically inconsequential
                out.write(currentFormat.lineSeparator);
            } else {
                // XXX if we get here then we have a illegal content, for
                //     now we'll just ignore it
            }

            newline(out);
            indent(out, 0);
        }

        // Output final line separator
        // We output this no matter what the newline flags say
        out.write(currentFormat.lineSeparator);
    }

    /**
     * Print out the <code>{@link DocType}</code>.
     *
     * @param doctype <code>DocType</code> to output.
     * @param out     <code>Writer</code> to use.
     */
    public void output(DocType doctype, Writer out) throws IOException {
        printDocType(out, doctype);
    }

    /**
     * Print out an <code>{@link Element}</code>, including
     * its <code>{@link Attribute}</code>s, and all
     * contained (child) elements, etc.
     *
     * @param element <code>Element</code> to output.
     * @param out     <code>Writer</code> to use.
     */
    public void output(Element element, Writer out) throws IOException {
        // If this is the root element we could pre-initialize the
        // namespace stack with the namespaces
        printElement(out, element, 0, createNamespaceStack());
    }

    /**
     * This will handle printing out an <code>{@link
     * Element}</code>'s content only, not including its tag, and
     * attributes.  This can be useful for printing the content of an
     * element that contains HTML, like "&lt;description&gt;JDOM is
     * &lt;b&gt;fun&gt;!&lt;/description&gt;".
     *
     * @param element <code>Element</code> to output.
     * @param out     <code>Writer</code> to use.
     */
    public void outputElementContent(Element element, Writer out) throws IOException {
        List<?> content = element.getContent();
        printContentRange(out, content, 0, content.size(), 0, createNamespaceStack());
    }

    /**
     * This will handle printing out a list of nodes.
     * This can be useful for printing the content of an element that
     * contains HTML, like "&lt;description&gt;JDOM is
     * &lt;b&gt;fun&gt;!&lt;/description&gt;".
     *
     * @param list <code>List</code> of nodes.
     * @param out  <code>Writer</code> to use.
     */
    public void output(List<?> list, Writer out) throws IOException {
        printContentRange(out, list, 0, list.size(), 0, createNamespaceStack());
    }

    /**
     * Print out a <code>{@link CDATA}</code> node.
     *
     * @param cdata <code>CDATA</code> to output.
     * @param out   <code>Writer</code> to use.
     */
    public void output(CDATA cdata, Writer out) throws IOException {
        printCDATA(out, cdata);
    }

    /**
     * Print out a <code>{@link Text}</code> node.  Perfoms
     * the necessary entity escaping and whitespace stripping.
     *
     * @param text <code>Text</code> to output.
     * @param out  <code>Writer</code> to use.
     */
    public void output(Text text, Writer out) throws IOException {
        printText(out, text);
    }

    /**
     * Print out a <code>{@link Comment}</code>.
     *
     * @param comment <code>Comment</code> to output.
     * @param out     <code>Writer</code> to use.
     */
    public void output(Comment comment, Writer out) throws IOException {
        printComment(out, comment);
    }

    /**
     * Print out a <code>{@link ProcessingInstruction}</code>.
     *
     * @param pi  <code>ProcessingInstruction</code> to output.
     * @param out <code>Writer</code> to use.
     */
    public void output(ProcessingInstruction pi, Writer out) throws IOException {
        boolean currentEscapingPolicy = currentFormat.ignoreTrAXEscapingPIs;

        // Output PI verbatim, disregarding TrAX escaping PIs.
        currentFormat.setIgnoreTrAXEscapingPIs(true);
        printProcessingInstruction(out, pi);
        currentFormat.setIgnoreTrAXEscapingPIs(currentEscapingPolicy);
    }

    /**
     * Print out a <code>{@link EntityRef}</code>.
     *
     * @param entity <code>EntityRef</code> to output.
     * @param out    <code>Writer</code> to use.
     */
    public void output(EntityRef entity, Writer out) throws IOException {
        printEntityRef(out, entity);
    }

    // * * * * * * * * * * Output to a String * * * * * * * * * *
    // * * * * * * * * * * Output to a String * * * * * * * * * *

    /**
     * Return a string representing a document.  Uses an internal
     * StringWriter. Warning: a String is Unicode, which may not match
     * the outputter's specified encoding.
     *
     * @param doc <code>Document</code> to format.
     */
    public String outputString(Document doc) {
        StringWriter out = new StringWriter();
        try {
            output(doc, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    /**
     * Return a string representing a DocType. Warning: a String is
     * Unicode, which may not match the outputter's specified
     * encoding.
     *
     * @param doctype <code>DocType</code> to format.
     */
    public String outputString(DocType doctype) {
        StringWriter out = new StringWriter();
        try {
            output(doctype, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    /**
     * Return a string representing an element. Warning: a String is
     * Unicode, which may not match the outputter's specified
     * encoding.
     *
     * @param element <code>Element</code> to format.
     */
    public String outputString(Element element) {
        StringWriter out = new StringWriter();
        try {
            output(element, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    /**
     * Return a string representing a list of nodes.  The list is
     * assumed to contain legal JDOM nodes.
     *
     * @param list <code>List</code> to format.
     */
    public String outputString(List<?> list) {
        StringWriter out = new StringWriter();
        try {
            output(list, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    /**
     * Return a string representing a CDATA node. Warning: a String is
     * Unicode, which may not match the outputter's specified
     * encoding.
     *
     * @param cdata <code>CDATA</code> to format.
     */
    public String outputString(CDATA cdata) {
        StringWriter out = new StringWriter();
        try {
            output(cdata, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    /**
     * Return a string representing a Text node. Warning: a String is
     * Unicode, which may not match the outputter's specified
     * encoding.
     *
     * @param text <code>Text</code> to format.
     */
    public String outputString(Text text) {
        StringWriter out = new StringWriter();
        try {
            output(text, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    /**
     * Return a string representing a comment. Warning: a String is
     * Unicode, which may not match the outputter's specified
     * encoding.
     *
     * @param comment <code>Comment</code> to format.
     */
    public String outputString(Comment comment) {
        StringWriter out = new StringWriter();
        try {
            output(comment, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    /**
     * Return a string representing a PI. Warning: a String is
     * Unicode, which may not match the outputter's specified
     * encoding.
     *
     * @param pi <code>ProcessingInstruction</code> to format.
     */
    public String outputString(ProcessingInstruction pi) {
        StringWriter out = new StringWriter();
        try {
            output(pi, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    /**
     * Return a string representing an entity. Warning: a String is
     * Unicode, which may not match the outputter's specified
     * encoding.
     *
     * @param entity <code>EntityRef</code> to format.
     */
    public String outputString(EntityRef entity) {
        StringWriter out = new StringWriter();
        try {
            output(entity, out); // output() flushes
        } catch (IOException e) {
        }
        return out.toString();
    }

    // * * * * * * * * * * Internal printing methods * * * * * * * * * *
    // * * * * * * * * * * Internal printing methods * * * * * * * * * *

    /**
     * This will handle printing of the declaration.
     * Assumes XML version 1.0 since we don't directly know.
     *
     * @param doc      <code>Document</code> whose declaration to write.
     * @param out      <code>Writer</code> to use.
     * @param encoding The encoding to add to the declaration
     */
    protected void printDeclaration(Writer out, Document doc, String encoding) throws IOException {

        // Only print the declaration if it's not being omitted
        if (!userFormat.omitDeclaration) {
            // Assume 1.0 version
            out.write("<?xml version=\"1.0\"");
            if (!userFormat.omitEncoding) {
                out.write(" encoding=\"" + encoding + "\"");
            }
            out.write("?>");

            // Print new line after decl always, even if no other new lines
            // Helps the output look better and is semantically
            // inconsequential
            out.write(currentFormat.lineSeparator);
        }
    }

    /**
     * This handle printing the DOCTYPE declaration if one exists.
     *
     * @param docType <code>Document</code> whose declaration to write.
     * @param out     <code>Writer</code> to use.
     */
    protected void printDocType(Writer out, DocType docType) throws IOException {

        String publicID = docType.getPublicID();
        String systemID = docType.getSystemID();
        String internalSubset = docType.getInternalSubset();
        boolean hasPublic = false;

        out.write("<!DOCTYPE ");
        out.write(docType.getElementName());
        if (publicID != null) {
            out.write(" PUBLIC \"");
            out.write(publicID);
            out.write("\"");
            hasPublic = true;
        }
        if (systemID != null) {
            if (!hasPublic) {
                out.write(" SYSTEM");
            }
            out.write(" \"");
            out.write(systemID);
            out.write("\"");
        }
        if ((internalSubset != null) && (!internalSubset.equals(""))) {
            out.write(" [");
            out.write(currentFormat.lineSeparator);
            out.write(docType.getInternalSubset());
            out.write("]");
        }
        out.write(">");
    }

    /**
     * This will handle printing of comments.
     *
     * @param comment <code>Comment</code> to write.
     * @param out     <code>Writer</code> to use.
     */
    protected void printComment(Writer out, Comment comment) throws IOException {
        out.write("<!--");
        out.write(StringUtils.unifyLineSeparators(comment.getText(), currentFormat.lineSeparator));
        out.write("-->");
    }

    /**
     * This will handle printing of processing instructions.
     *
     * @param pi  <code>ProcessingInstruction</code> to write.
     * @param out <code>Writer</code> to use.
     */
    protected void printProcessingInstruction(Writer out, ProcessingInstruction pi) throws IOException {
        String target = pi.getTarget();
        boolean piProcessed = false;

        if (!currentFormat.ignoreTrAXEscapingPIs) {
            if (target.equals(Result.PI_DISABLE_OUTPUT_ESCAPING)) {
                escapeOutput = false;
                piProcessed = true;
            } else if (target.equals(Result.PI_ENABLE_OUTPUT_ESCAPING)) {
                escapeOutput = true;
                piProcessed = true;
            }
        }
        if (!piProcessed) {
            String rawData = pi.getData();

            // Write <?target data?> or if no data then just <?target?>
            if (!"".equals(rawData)) {
                out.write("<?");
                out.write(target);
                out.write(" ");
                out.write(rawData);
                out.write("?>");
            } else {
                out.write("<?");
                out.write(target);
                out.write("?>");
            }
        }
    }

    /**
     * This will handle printing a <code>{@link EntityRef}</code>.
     * Only the entity reference such as <code>&amp;entity;</code>
     * will be printed. However, subclasses are free to override
     * this method to print the contents of the entity instead.
     *
     * @param entity <code>EntityRef</code> to output.
     * @param out    <code>Writer</code> to use.
     */
    protected void printEntityRef(Writer out, EntityRef entity) throws IOException {
        out.write("&");
        out.write(entity.getName());
        out.write(";");
    }

    /**
     * This will handle printing of <code>{@link CDATA}</code> text.
     *
     * @param cdata <code>CDATA</code> to output.
     * @param out   <code>Writer</code> to use.
     */
    protected void printCDATA(Writer out, CDATA cdata) throws IOException {
        String str = (currentFormat.mode == Format.TextMode.NORMALIZE)
                ? cdata.getTextNormalize()
                : ((currentFormat.mode == Format.TextMode.TRIM)
                        ? cdata.getText().trim()
                        : cdata.getText());
        out.write("<![CDATA[");
        out.write(str);
        out.write("]]>");
    }

    /**
     * This will handle printing of <code>{@link Text}</code> strings.
     *
     * @param text <code>Text</code> to write.
     * @param out  <code>Writer</code> to use.
     */
    protected void printText(Writer out, Text text) throws IOException {
        String str = (currentFormat.mode == Format.TextMode.NORMALIZE)
                ? text.getTextNormalize()
                : ((currentFormat.mode == Format.TextMode.TRIM) ? text.getText().trim() : text.getText());
        out.write(escapeElementEntities(str));
    }

    /**
     * This will handle printing a string.  Escapes the element entities,
     * trims interior whitespace, etc. if necessary.
     */
    private void printString(Writer out, String str) throws IOException {
        if (currentFormat.mode == Format.TextMode.NORMALIZE) {
            str = Text.normalizeString(str);
        } else if (currentFormat.mode == Format.TextMode.TRIM) {
            str = str.trim();
        }
        out.write(escapeElementEntities(str));
    }

    /**
     * This will handle printing of a <code>{@link Element}</code>,
     * its <code>{@link Attribute}</code>s, and all contained (child)
     * elements, etc.
     *
     * @param element    <code>Element</code> to output.
     * @param out        <code>Writer</code> to use.
     * @param level      <code>int</code> level of indention.
     * @param namespaces <code>List</code> stack of Namespaces in scope.
     */
    protected void printElement(Writer out, Element element, int level, NamespaceStack namespaces) throws IOException {

        List<?> attributes = element.getAttributes();
        List<?> content = element.getContent();

        // Check for xml:space and adjust format settings
        String space = null;
        if (attributes != null) {
            space = element.getAttributeValue("space", Namespace.XML_NAMESPACE);
        }

        Format previousFormat = currentFormat;

        if ("default".equals(space)) {
            currentFormat = userFormat;
        } else if ("preserve".equals(space)) {
            currentFormat = PRESERVE_FORMAT;
        }

        // Print the beginning of the tag plus attributes and any
        // necessary namespace declarations
        out.write("<");
        printQualifiedName(out, element);

        // Mark our namespace starting point
        int previouslyDeclaredNamespaces = namespaces.size();

        // Print the element's namespace, if appropriate
        printElementNamespace(out, element, namespaces);

        // Print out additional namespace declarations
        printAdditionalNamespaces(out, element, namespaces);

        // Print out attributes
        if (attributes != null) {
            printAttributes(out, attributes, element, namespaces);
        }

        // Depending on the settings (newlines, textNormalize, etc), we may
        // or may not want to print all of the content, so determine the
        // index of the start of the content we're interested
        // in based on the current settings.

        int start = skipLeadingWhite(content, 0);
        int size = content.size();
        if (start >= size) {
            // Case content is empty or all insignificant whitespace
            if (currentFormat.expandEmptyElements) {
                out.write("></");
                printQualifiedName(out, element);
                out.write(">");
            } else {
                out.write(" />");
            }
        } else {
            out.write(">");

            // For a special case where the content is only CDATA
            // or Text we don't want to indent after the start or
            // before the end tag.

            if (nextNonText(content, start) < size) {
                // Case Mixed Content - normal indentation
                newline(out);
                printContentRange(out, content, start, size, level + 1, namespaces);
                newline(out);
                indent(out, level);
            } else {
                // Case all CDATA or Text - no indentation
                printTextRange(out, content, start, size);
            }
            out.write("</");
            printQualifiedName(out, element);
            out.write(">");
        }

        // remove declared namespaces from stack
        while (namespaces.size() > previouslyDeclaredNamespaces) {
            namespaces.pop();
        }

        // Restore our format settings
        currentFormat = previousFormat;
    }

    /**
     * This will handle printing of content within a given range.
     * The range to print is specified in typical Java fashion; the
     * starting index is inclusive, while the ending index is
     * exclusive.
     *
     * @param content    <code>List</code> of content to output
     * @param start      index of first content node (inclusive.
     * @param end        index of last content node (exclusive).
     * @param out        <code>Writer</code> to use.
     * @param level      <code>int</code> level of indentation.
     * @param namespaces <code>List</code> stack of Namespaces in scope.
     */
    private void printContentRange(
            Writer out, List<?> content, int start, int end, int level, NamespaceStack namespaces) throws IOException {
        boolean firstNode; // Flag for 1st node in content
        Object next; // Node we're about to print
        int first, index; // Indexes into the list of content

        index = start;
        while (index < end) {
            firstNode = index == start;
            next = content.get(index);

            //
            // Handle consecutive CDATA, Text, and EntityRef nodes all at once
            //
            if ((next instanceof Text) || (next instanceof EntityRef)) {
                first = skipLeadingWhite(content, index);
                // Set index to next node for loop
                index = nextNonText(content, first);

                // If it's not all whitespace - print it!
                if (first < index) {
                    if (!firstNode) {
                        newline(out);
                    }
                    indent(out, level);
                    printTextRange(out, content, first, index);
                }
                continue;
            }

            //
            // Handle other nodes
            //
            if (!firstNode) {
                newline(out);
            }

            indent(out, level);

            if (next instanceof Comment) {
                printComment(out, (Comment) next);
            } else if (next instanceof Element) {
                printElement(out, (Element) next, level, namespaces);
            } else if (next instanceof ProcessingInstruction) {
                printProcessingInstruction(out, (ProcessingInstruction) next);
            } else {
                // XXX if we get here then we have a illegal content, for
                //     now we'll just ignore it (probably should throw
                //     a exception)
            }

            index++;
        } /* while */
    }

    /**
     * This will handle printing of a sequence of <code>{@link CDATA}</code>
     * or <code>{@link Text}</code> nodes.  It is an error to have any other
     * pass this method any other type of node.
     *
     * @param content <code>List</code> of content to output
     * @param start   index of first content node (inclusive).
     * @param end     index of last content node (exclusive).
     * @param out     <code>Writer</code> to use.
     */
    private void printTextRange(Writer out, List<?> content, int start, int end) throws IOException {
        String previous; // Previous text printed
        Object node; // Next node to print
        String next; // Next text to print

        previous = null;

        // Remove leading whitespace-only nodes
        start = skipLeadingWhite(content, start);

        int size = content.size();
        if (start < size) {
            // And remove trialing whitespace-only nodes
            end = skipTrailingWhite(content, end);

            for (int i = start; i < end; i++) {
                node = content.get(i);

                // Get the unmangled version of the text
                // we are about to print
                if (node instanceof CDATA) {
                    next = "<![CDATA[" + ((CDATA) node).getValue() + "]]>";
                } else if (node instanceof Text) {
                    next = ((Text) node).getText();
                } else if (node instanceof EntityRef) {
                    next = "&" + ((EntityRef) node).getValue() + ";";
                } else {
                    throw new IllegalStateException("Should see only CDATA, Text, or EntityRef");
                }

                // This may save a little time
                if (next == null || "".equals(next)) {
                    continue;
                }

                // Determine if we need to pad the output (padding is
                // only need in trim or normalizing mode)
                if (previous != null) { // Not 1st node
                    if (currentFormat.mode == Format.TextMode.NORMALIZE || currentFormat.mode == Format.TextMode.TRIM) {
                        if ((endsWithWhite(previous)) || (startsWithWhite(next))) {
                            out.write(" ");
                        }
                    }
                }

                // Print the node
                if (node instanceof CDATA) {
                    printCDATA(out, (CDATA) node);
                } else if (node instanceof EntityRef) {
                    printEntityRef(out, (EntityRef) node);
                } else {
                    printString(out, next);
                }

                previous = next;
            }
        }
    }

    /**
     * This will handle printing of any needed <code>{@link Namespace}</code>
     * declarations.
     *
     * @param ns  <code>Namespace</code> to print definition of
     * @param out <code>Writer</code> to use.
     */
    private void printNamespace(Writer out, Namespace ns, NamespaceStack namespaces) throws IOException {
        String prefix = ns.getPrefix();
        String uri = ns.getURI();

        // Already printed namespace decl?
        if (uri.equals(namespaces.getURI(prefix))) {
            return;
        }

        out.write(" xmlns");
        if (!prefix.equals("")) {
            out.write(":");
            out.write(prefix);
        }
        out.write("=\"");
        out.write(uri);
        out.write("\"");
        namespaces.push(ns);
    }

    /**
     * This will handle printing of a <code>{@link Attribute}</code> list.
     *
     * @param attributes <code>List</code> of Attribute objcts
     * @param out        <code>Writer</code> to use
     */
    protected void printAttributes(Writer out, List<?> attributes, Element parent, NamespaceStack namespaces)
            throws IOException {

        // I do not yet handle the case where the same prefix maps to
        // two different URIs. For attributes on the same element
        // this is illegal; but as yet we don't throw an exception
        // if someone tries to do this
        // Set prefixes = new HashSet();
        for (int i = 0; i < attributes.size(); i++) {
            Attribute attribute = (Attribute) attributes.get(i);
            Namespace ns = attribute.getNamespace();
            if ((ns != Namespace.NO_NAMESPACE) && (ns != Namespace.XML_NAMESPACE)) {
                printNamespace(out, ns, namespaces);
            }

            out.write(" ");
            printQualifiedName(out, attribute);
            out.write("=");

            out.write("\"");
            out.write(escapeAttributeEntities(attribute.getValue()));
            out.write("\"");
        }
    }

    private void printElementNamespace(Writer out, Element element, NamespaceStack namespaces) throws IOException {
        // Add namespace decl only if it's not the XML namespace and it's
        // not the NO_NAMESPACE with the prefix "" not yet mapped
        // (we do output xmlns="" if the "" prefix was already used and we
        // need to reclaim it for the NO_NAMESPACE)
        Namespace ns = element.getNamespace();
        if (ns == Namespace.XML_NAMESPACE) {
            return;
        }
        if (!((ns == Namespace.NO_NAMESPACE) && (namespaces.getURI("") == null))) {
            printNamespace(out, ns, namespaces);
        }
    }

    private void printAdditionalNamespaces(Writer out, Element element, NamespaceStack namespaces) throws IOException {
        List<?> list = element.getAdditionalNamespaces();
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                Namespace additional = (Namespace) list.get(i);
                printNamespace(out, additional, namespaces);
            }
        }
    }

    // * * * * * * * * * * Support methods * * * * * * * * * *
    // * * * * * * * * * * Support methods * * * * * * * * * *

    /**
     * This will print a new line only if the newlines flag was set to
     * true.
     *
     * @param out <code>Writer</code> to use
     */
    private void newline(Writer out) throws IOException {
        if (currentFormat.indent != null) {
            out.write(currentFormat.lineSeparator);
        }
    }

    /**
     * This will print indents (only if the newlines flag was
     * set to <code>true</code>, and indent is non-null).
     *
     * @param out   <code>Writer</code> to use
     * @param level current indent level (number of tabs)
     */
    private void indent(Writer out, int level) throws IOException {
        if (currentFormat.indent == null || currentFormat.indent.equals("")) {
            return;
        }

        for (int i = 0; i < level; i++) {
            out.write(currentFormat.indent);
        }
    }

    // Returns the index of the first non-all-whitespace CDATA or Text,
    // index = content.size() is returned if content contains
    // all whitespace.
    // @param start index to begin search (inclusive)
    private int skipLeadingWhite(List<?> content, int start) {
        if (start < 0) {
            start = 0;
        }

        int index = start;
        int size = content.size();
        if (currentFormat.mode == Format.TextMode.TRIM_FULL_WHITE
                || currentFormat.mode == Format.TextMode.NORMALIZE
                || currentFormat.mode == Format.TextMode.TRIM) {
            while (index < size) {
                if (!isAllWhitespace(content.get(index))) {
                    return index;
                }
                index++;
            }
        }
        return index;
    }

    // Return the index + 1 of the last non-all-whitespace CDATA or
    // Text node,  index < 0 is returned
    // if content contains all whitespace.
    // @param start index to begin search (exclusive)
    private int skipTrailingWhite(List<?> content, int start) {
        int size = content.size();
        if (start > size) {
            start = size;
        }

        int index = start;
        if (currentFormat.mode == Format.TextMode.TRIM_FULL_WHITE
                || currentFormat.mode == Format.TextMode.NORMALIZE
                || currentFormat.mode == Format.TextMode.TRIM) {
            while (index >= 0) {
                if (!isAllWhitespace(content.get(index - 1))) {
                    break;
                }
                --index;
            }
        }
        return index;
    }

    // Return the next non-CDATA, non-Text, or non-EntityRef node,
    // index = content.size() is returned if there is no more non-CDATA,
    // non-Text, or non-EntiryRef nodes
    // @param start index to begin search (inclusive)
    private static int nextNonText(List<?> content, int start) {
        if (start < 0) {
            start = 0;
        }

        int index = start;
        int size = content.size();
        while (index < size) {
            Object node = content.get(index);
            if (!((node instanceof Text) || (node instanceof EntityRef))) {
                return index;
            }
            index++;
        }
        return size;
    }

    // Determine if a Object is all whitespace
    private boolean isAllWhitespace(Object obj) {
        String str = null;

        if (obj instanceof String) {
            str = (String) obj;
        } else if (obj instanceof Text) {
            str = ((Text) obj).getText();
        } else {
            return false;
        }

        for (int i = 0; i < str.length(); i++) {
            if (!isWhitespace(str.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    // Determine if a string starts with a XML whitespace.
    private boolean startsWithWhite(String str) {
        return ((str != null) && (!str.isEmpty()) && isWhitespace(str.charAt(0)));
    }

    // Determine if a string ends with a XML whitespace.
    private boolean endsWithWhite(String str) {
        return ((str != null) && (!str.isEmpty()) && isWhitespace(str.charAt(str.length() - 1)));
    }

    // Determine if a character is a XML whitespace.
    // XXX should this method be in Verifier
    private static boolean isWhitespace(char c) {
        return (c == ' ' || c == '\n' || c == '\t' || c == '\r');
    }

    /**
     * This will take the pre-defined entities in XML 1.0 and
     * convert their character representation to the appropriate
     * entity reference, suitable for XML attributes.  It does not convert
     * the single quote (') because it's not necessary as the outputter
     * writes attributes surrounded by double-quotes.
     *
     * @param str <code>String</code> input to escape.
     * @return <code>String</code> with escaped content.
     */
    public String escapeAttributeEntities(String str) {
        StringBuilder buffer;
        char ch;
        String entity;
        EscapeStrategy strategy = currentFormat.escapeStrategy;

        buffer = null;
        for (int i = 0; i < str.length(); i++) {
            ch = str.charAt(i);
            switch (ch) {
                case '<':
                    entity = "&lt;";
                    break;
                case '>':
                    entity = "&gt;";
                    break;
                    /*
                                    case '\'' :
                                        entity = "&apos;";
                                        break;
                    */
                case '\"':
                    entity = "&quot;";
                    break;
                case '&':
                    entity = "&amp;";
                    break;
                case '\r':
                    entity = "&#xD;";
                    break;
                case '\t':
                    entity = "&#x9;";
                    break;
                case '\n':
                    entity = "&#xA;";
                    break;
                default:
                    if (strategy.shouldEscape(ch)) {
                        entity = "&#x" + Integer.toHexString(ch) + ";";
                    } else {
                        entity = null;
                    }
                    break;
            }
            if (buffer == null) {
                if (entity != null) {
                    // An entity occurred, so we'll have to use StringBuilder
                    // (allocate room for it plus a few more entities).
                    buffer = new StringBuilder(str.length() + 20);
                    // Copy previous skipped characters and fall through
                    // to pickup current character
                    buffer.append(str.substring(0, i));
                    buffer.append(entity);
                }
            } else {
                if (entity == null) {
                    buffer.append(ch);
                } else {
                    buffer.append(entity);
                }
            }
        }

        // If there were any entities, return the escaped characters
        // that we put in the StringBuilder. Otherwise, just return
        // the unmodified input string.
        return (buffer == null) ? str : buffer.toString();
    }

    /**
     * This will take the three pre-defined entities in XML 1.0
     * (used specifically in XML elements) and convert their character
     * representation to the appropriate entity reference, suitable for
     * XML element content.
     *
     * @param str <code>String</code> input to escape.
     * @return <code>String</code> with escaped content.
     */
    public String escapeElementEntities(String str) {
        if (!escapeOutput) {
            return str;
        }

        StringBuilder buffer;
        char ch;
        String entity;
        EscapeStrategy strategy = currentFormat.escapeStrategy;

        buffer = null;
        for (int i = 0; i < str.length(); i++) {
            ch = str.charAt(i);
            switch (ch) {
                case '<':
                    entity = "&lt;";
                    break;
                case '>':
                    entity = "&gt;";
                    break;
                case '&':
                    entity = "&amp;";
                    break;
                case '\r':
                    entity = "&#xD;";
                    break;
                case '\n':
                    entity = currentFormat.lineSeparator;
                    break;
                default:
                    if (strategy.shouldEscape(ch)) {
                        entity = "&#x" + Integer.toHexString(ch) + ";";
                    } else {
                        entity = null;
                    }
                    break;
            }
            if (buffer == null) {
                if (entity != null) {
                    // An entity occurred, so we'll have to use StringBuilder
                    // (allocate room for it plus a few more entities).
                    buffer = new StringBuilder(str.length() + 20);
                    // Copy previous skipped characters and fall through
                    // to pickup current character
                    buffer.append(str.substring(0, i));
                    buffer.append(entity);
                }
            } else {
                if (entity == null) {
                    buffer.append(ch);
                } else {
                    buffer.append(entity);
                }
            }
        }

        // If there were any entities, return the escaped characters
        // that we put in the StringBuilder. Otherwise, just return
        // the unmodified input string.
        return (buffer == null) ? str : buffer.toString();
    }

    /** Returns a copy of this XMLOutputter. */
    @Override
    public Object clone() {
        // Implementation notes: Since all state of an XMLOutputter is
        // embodied in simple private instance variables, Object.clone
        // can be used.  Note that since Object.clone is totally
        // broken, we must catch an exception that will never be
        // thrown.
        try {
            return super.clone();
        } catch (java.lang.CloneNotSupportedException e) {
            // even though this should never ever happen, it's still
            // possible to fool Java into throwing a
            // CloneNotSupportedException.  If that happens, we
            // shouldn't swallow it.
            throw new RuntimeException(e.toString());
        }
    }

    /**
     * Return a string listing of the settings for this
     * XMLOutputter instance.
     *
     * @return a string listing the settings for this XMLOutputter instance
     */
    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        for (int i = 0; i < userFormat.lineSeparator.length(); i++) {
            char ch = userFormat.lineSeparator.charAt(i);
            switch (ch) {
                case '\r':
                    buffer.append("\\r");
                    break;
                case '\n':
                    buffer.append("\\n");
                    break;
                case '\t':
                    buffer.append("\\t");
                    break;
                default:
                    buffer.append("[" + ((int) ch) + "]");
                    break;
            }
        }

        return ("XMLOutputter[omitDeclaration = " + userFormat.omitDeclaration + ", "
                + "encoding = " + userFormat.encoding + ", "
                + "omitEncoding = " + userFormat.omitEncoding + ", "
                + "indent = '" + userFormat.indent + "'" + ", "
                + "expandEmptyElements = " + userFormat.expandEmptyElements + ", "
                + "lineSeparator = '" + buffer + "', "
                + "textMode = " + userFormat.mode + "]");
    }

    /**
     * Factory for making new NamespaceStack objects.  The NamespaceStack
     * created is actually an inner class extending the package protected
     * NamespaceStack, as a way to make NamespaceStack "friendly" toward
     * subclassers.
     */
    private NamespaceStack createNamespaceStack() {
        // actually returns a XMLOutputter.NamespaceStack (see below)
        return new NamespaceStack();
    }

    /**
     * Our own null subclass of NamespaceStack.  This plays a little
     * trick with Java access protection.  We want subclasses of
     * XMLOutputter to be able to override protected methods that
     * declare a NamespaceStack parameter, but we don't want to
     * declare the parent NamespaceStack class as public.
     */
    protected static class NamespaceStack extends org.apache.maven.archetype.common.util.NamespaceStack {}

    // Support method to print a name without using elt.getQualifiedName()
    // and thus avoiding a StringBuilder creation and memory churn
    private void printQualifiedName(Writer out, Element e) throws IOException {
        if (e.getNamespace().getPrefix().isEmpty()) {
            out.write(e.getName());
        } else {
            out.write(e.getNamespace().getPrefix());
            out.write(':');
            out.write(e.getName());
        }
    }

    // Support method to print a name without using att.getQualifiedName()
    // and thus avoiding a StringBuilder creation and memory churn
    private void printQualifiedName(Writer out, Attribute a) throws IOException {
        String prefix = a.getNamespace().getPrefix();
        if ((prefix != null) && (!prefix.equals(""))) {
            out.write(prefix);
            out.write(':');
            out.write(a.getName());
        } else {
            out.write(a.getName());
        }
    }

    // * * * * * * * * * * Deprecated methods * * * * * * * * * *

    /* The methods below here are deprecations of protected methods.  We
     * don't usually deprecate protected methods, so they're commented out.
     * They're left here in case this mass deprecation causes people trouble.
     * Since we're getting close to 1.0 it's actually better for people to
     * raise issues early though.
     */

}
